Taeseong Blog

V8 엔진의 동작원리와 최적화 방법

2025-04-14

V8 엔진최적화

V8 엔진이란

V8은 구글에서 개발한 오픈 소스 자바스크립트 엔진으로 웹 브라우저에서 자바스크립트 코드를 실행하는 데 사용됨. 현재 크롬, 사파리를 포함한 여러 브라우저가 V8 엔진을 사용해 코드를 실행하기 전에 기계어로 번역함.

V8은 속도 향상을 위해 JIT(Just In Time) 컴파일러를 사용해 코드를 실행하기 전에 기계어로 번역함.

JIT는 실행 중에 최적화가 가능한 코드를 Turbo Fan 이용해 최적화 함.

JIT의 동작 원리

1. 소스 코드 파싱

  • 자바스크립트 소스 코드를 입력받아 파싱함.
  • Parser가 코드를 토큰으로 분해하고, 이를 기반으로 AST(Abstract Syntax Tree)를 생성함.
  • AST는 코드의 구조를 트리 형태로 표현한 것으로, 이후 컴파일링과 실행의 기반이 됨.

2. 인터프리터를 통한 초기 실행

  • V8의 인터프리터인 Ignition이 AST를 바탕으로 코드를 바이트코드로 변환해 즉시 실행함.
  • 이 과정에서 코드 실행 패턴(예: 반복 호출 빈도, 변수 타입)을 수집해 프로파일링 데이터를 생성함.
  • 초기 실행 속도를 높이기 위해 빠르게 바이트코드를 실행하지만, 최적화는 아직 이루어지지 않음.

3. 프로파일링과 JIT 컴파일링

  • 프로파일링 데이터를 기반으로 V8은 자주 실행되는 코드를 식별함.
  • 자주 실행되는 핫 코드 경로는 TurboFan(JIT 컴파일러)으로 보내짐.
  • TurboFan은 프로파일링 데이터를 활용해 중간 표현(Intermediate Representation, IR)을 생성하고, 이를 최적화된 기계어로 컴파일함.
  • 최적화 기법으로는 인라인 캐싱(Inline Caching), 히든 클래스(Hidden Classes), 반복 루프 최적화 등이 사용됨.

4. 최적화된 코드 실행

  • TurboFan이 생성한 최적화된 기계어 코드를 메모리에 로드하고 실행함.
  • 최적화된 코드는 실행 속도가 훨씬 빠르며, 이후 동일한 코드가 호출될 때 재사용됨.
  • 만약 코드 실행 중 변수 타입이 변경되거나 가정이 깨지면(예: 동적 타입 변경), V8은 디옵티마이제이션을 수행해 인터프리터로 돌아가 재컴파일함.

5. 가비지 컬렉션

  • V8은 메모리 관리를 위해 가비지 컬렉터를 사용함.
  • Orinoco라는 가비지 컬렉터가 메모리 누수를 방지하며, 사용되지 않는 객체를 주기적으로 정리함.
while(b !== 0){
    if(a > b) a = a - b;
    else b = b - a;
}
return a;

AST

인라인 캐싱

// 같은 함수를 5억번 실행하는 코드
// [A] 최적화되지 않은 예시
(() => {
  const jongu = { firstname: "Jongu", lastname: "Lee", job: "developer" };
  const hyunwoo = { firstname: "Hyunwoo", lastname: "Choi", gender: "male" };
  const yujin = { firstname: "Yujin", lastname: "Kwon", hobby: "drawing" };
  const minji = { firstname: "Minji", lastname: "Kim", nickname: "MJ" };
  
  // 사람의 이름을 출력하는 함수
  const getFullName = (user) => `${user.lastname} ${user.firstname}`;

  const people = [jongu, minwoo, hyunwoo, yujin, minji];

  console.time("최적화 되지 않은 코드");
  for (let i = 0; i < 500_000_000; i++) {
    getFullName(people[i % 5]);
  }
  console.timeEnd("최적화 되지 않은 코드");
})();
// 결과 - 최적화 되지 않은 코드: 10.085s

// [B] 최적화된 예시
(() => {
  const jongu = { firstname: "a", lastname: "Lee", age: 30, job: "developer" };
  const minwoo = { firstname: "abcd", lastname: "Park", age: 33, job: "developer" };
  const hyunwoo = { firstname: "Hyunwoo", lastname: "Choi", age: 29, job: "developer" };
  const yujin = { firstname: "Yujin", lastname: "Kwon", age: 21, job: "developer" };
  consc사람의 이름을 출력하는 함수
  const getFullName = (user) => `${user.lastname} ${user.firstname}`;

  const people = [jongu, minwoo, hyunwoo, yujin, minji];

  console.time("최적화된 코드");
  for (let i = 0; i < 500_000_000; i++) {
    getFullName(people[i % 5]);
  }
  console.timeEnd("최적화된 코드");
})();
// 결과 - 최적화된 코드: 5.784s

위 코드 A, B의 다른 점은 사용자 객체의 구조뿐이다.

속성 한 개가 추가됨으로 인해서 속도는 약 2배가량의 차이를 보인다는 점

인라인 캐시는 최적화 과정에서 객체의 attribute에 접근하는 부분에 실제 메모리 주소를 할당하여 lookup 과정을 생략한다. 따라서 반복적으로 접근하는 객체의 구조가 동일해야 객체를 구분하는 과정을 생략하고 offset을 적용하므로 최적화에 용이하다.

위 코드에서 user.lastname의 attribute 접근 과정에서 해당 object에서 lastname과 firstname 이라는 attribute의 위치를 offset으로 저장하여, static 하게 최적화 코드에 넣는다.

히든 클래스

자바스크립트는 동적 언어이며 객체가 생성된 이후에도 속성을 쉽게 추가하거나 삭제할 수 있다.

히든클래스는 객체지향 언어에서 사용되는 고정 객체 레이아웃과 유사하게 작동하는데, 런타임에 생성된다는 차이점이 있다. V8 엔진은 히든클래스를 활용하여 객체의 프로퍼티 구조를 효율적으로 처리한다.

터보팬은 히든 클래스 정보를 활용하여 객체에 대한 프로퍼티 접근을 최적화한다.

function Point(x, y) {
    this.first = x;             // B
    this.second = y;            // C
}

const p1 = new Point(1, 2);     // A
  1. A 과정에서 V8 엔진은 C0 히든클래스를 생성하고, p1 객체는 C0를 참조.

  2. B 과정에서 V8 엔진은 C0를 기반으로 C1이라는 두 번째 히든클래스를 생성.

  • property table: 프로퍼티가 메모리의 어디에 있는 찾기 쉽게 알려주는 이정표

  • transition table: 구조가 변하면 어디로 가야 하는지 알려주는 이정표

  1. C 과정에서 V8 엔진은 C2 히든클래스를 만들고 클래스 전환을 통해서 업데이트

위와 같은 과정을 거치며 히든클래스들이 생성되는데 개발 시 주의할 점은 새로운 히든 클래스를 생성할 때마다 기존에 생성된 히든클래스를 이용할 수 있도록 코드를 작성해야 한다는 점

아래 코드는 히든클래스 관점에서 좋지 않은 코드인데

function Point(x, y) {
    this.first = x;
}

const p1 = new Point(1);
const p2 = new Point(2);

p1.second = 10;
p1.third = 100;

p2.third = 100;
p2.second = 10;

결과적으로 p1과 p2는 동일한 데이터를 가지고 있지만 서로 다른 히든 클래스를 참조하게 됨.

위와 같은 코드는 같은 offset을 참조하지 못하므로 인라인 캐싱이 되지 않고 여러 개의 히든 클래스를 만들게 되는 문제를 발생시킴.